/*
 * Simple OpenGL 3 Tutorial.
 *
 * Sample program 03.
 */

#include <gl_app.h>

#include <gu_shader.h>

#include <gu_vertex_buffer.h>
#include <gu_util.h>

#include <Eigen/Dense>
#include <Eigen/Geometry>

#include <GL/glew.h>

#include <vector>

const char* windowTitle = "Program 03";

typedef std::unique_ptr<gu::Shader> ShaderPtr;
typedef std::unique_ptr<gu::VertexBuffer> VertexBufferPtr;
typedef std::unique_ptr<gu::IndexBuffer> IndexBufferPtr;
typedef Eigen::Vector4f vec4;
typedef Eigen::Vector3f vec3;

ShaderPtr shader;
VertexBufferPtr vertexBuffer;
IndexBufferPtr indexBuffer;
IndexBufferPtr lineIndexBuffer;

gu::BufferComposite buffers;


GLfloat Vertices[] = {
    -0.8f, -0.8f, 0.0f, 1.0f,
    0.0f,  0.8f, 0.0f, 1.0f,
    0.8f, -0.8f, 0.0f, 1.0f
};

GLfloat Colors[] = {
    1.0f, 0.0f, 0.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f, 1.0f
};

const char objectToWorldName[] = "objectToWorld";
const char worldToViewName[] = "worldToView";
const char projectionName[] = "projection";

const GLchar* VertexShader =
        "#version 330\n"
        "in vec4 in_Position;\n"
        "in vec4 in_Color;\n"
        "uniform  mat4 objectToWorld;\n"
        "uniform  mat4 worldToView;\n"
        "uniform  mat4 projection;\n"
        "out vec4 ex_Color;\n"
        "void main(void)\n"
        "{\n"
        "       gl_Position = projection * worldToView * objectToWorld * in_Position;\n"
        "       //gl_Position = in_Position;\n"
        "       ex_Color = in_Color;\n"
        "}\n";

const GLchar* FragmentShader =
        "#version 330\n"
        "in vec4 ex_Color;\n"
        "out vec4 out_Color;\n"
        "void main(void)\n"
        "{\n"
        "       out_Color = ex_Color;\n"
        "}\n";

unsigned FrameCount = 0;


GLuint VertexShaderId;
GLuint FragmentShaderId;
GLuint programId;


GLuint posLocation;
GLuint colLocation;

GLuint VaoId;
GLuint VboId;

GLuint ColorBufferId;

Eigen::Matrix4f objectToWorld;
Eigen::Matrix4f worldToView;
Eigen::Matrix4f projection;

enum MatrixId{ObjToWorld = 0, WorldToView = 1, Projection = 2, MatrixCount};

GLuint shaderIds[MatrixCount];
const char*  uniformNames[] = {objectToWorldName, worldToViewName,projectionName};

int prevTime = 0;

void initMatrices(gu::Shader* shader)
{
    objectToWorld = Eigen::Matrix4f::Identity();
    worldToView = Eigen::Matrix4f::Identity();
    projection = Eigen::Matrix4f::Identity();
}

void updateScene(double time){

    //angle += 0.01f;
    float scaleFreq = 0.001f;
    float rotFreq = 0.001f;
    float movFreq = 0.009f;

    time *= 0.05;

    float angle = (float) time * rotFreq;
    Eigen::Vector3f axis(0,0,1);
    Eigen::AngleAxisf rot(angle, axis);

    float dx = (float) sin(time * movFreq);
    Eigen::Translation3f mov(dx, 0.0f, 0.0f);

    float sx = (float) fabs(sin(time * scaleFreq ));
    float sy = sx;
    float sz = 1.0;
    auto scale = Eigen::Scaling(sx, sy, sz);

    Eigen::Transform<float, 3, Eigen::Affine> tfinal = rot * mov;
    tfinal =  mov * rot * scale;

    objectToWorld = tfinal.matrix();

    glUniformMatrix4fv(shaderIds[ObjToWorld], 1, GL_FALSE, objectToWorld.data());
    glUniformMatrix4fv(shaderIds[WorldToView], 1, GL_FALSE, worldToView.data());
    glUniformMatrix4fv(shaderIds[Projection], 1, GL_FALSE, projection.data());

}

struct VertexPN
{
    float pos[4];
    float nor[4];
    void assign(vec3& v, vec3& n)
    {
        for(int i = 0; i < 3; ++i)
        {
            pos[i] = v[i];
            nor[i] = n[i];
        }
    }
};


struct  Box
{
    vec3 origin_;
    vec3 dims_;

    VertexPN vertices[24];
    uint16_t indices[36];

    Box(vec3 origin, vec3 dims):origin_(origin), dims_(dims)
    {}

    void instantiate()
    {
    }

};


void instantiateVertexData(std::vector<VertexPN> vertexData)
{
}

/* Create the VBO to store thecolored triangle. */
void createTriangleVBO(void)
{

    vec4 a(0.5f, 0.0f, 0.0f, 1.0f);
    vec4 b(0.0f, 0.5f, 0.0f, 1.0f);
    vec4 c(0.0f, 0.0f, 0.5f, 1.0f);

    vec4 vertices;


    GLfloat vertexData[] = {
            -0.8f, -0.8f, 0.0f, 1.0f , 1.0f, 0.0f, 0.0f, 1.0f,
             0.0f,  0.8f, 0.0f, 1.0f , 0.0f, 1.0f, 0.0f, 1.0f,
             0.8f, -0.8f, 0.0f, 1.0f , 0.0f, 0.0f, 1.0f, 1.0f,
    };

    uint16_t indices[] = {0, 1, 2};
    uint16_t lineIndices[] = {0, 1, 2, 0};

    GLenum ErrorCheckValue = glGetError();

    vertexBuffer.reset(gu::newGlVertexBuffer());

    gu::StreamChannelInfo signature[] = {{gu::STREAM_FLOAT4, posLocation},{gu::STREAM_FLOAT4, colLocation}};
    size_t vertexSize = 8 * sizeof(float);

    vertexBuffer->setSignature(signature, 2, vertexSize);
    vertexBuffer->setData(reinterpret_cast<uint8_t*>(vertexData), sizeof(vertexData));
    vertexBuffer->init();

    indexBuffer.reset(gu::newGlIndexBuffer());
    indexBuffer->setData(indices, 3);
    indexBuffer->init();

    lineIndexBuffer.reset(gu::newGlIndexBuffer());
    lineIndexBuffer->setData(lineIndices, 4);
    lineIndexBuffer->layout(gu::LINE_STRIP);
    lineIndexBuffer->init();


    buffers.vertex(vertexBuffer.get());
    buffers.index(indexBuffer.get());
}

void destroyVBO(void)
{

}

void createShaders(void)
{

    shader.reset(gu::newShader(gu::GLSL_330));
    shader->setSource(gu::FRAGMENT, FragmentShader);
    shader->setSource(gu::VERTEX, VertexShader);

    if(! shader->compile())
    {
        exit(-1);
    }

    posLocation = shader->getAttribLocation("in_Position");
    colLocation = shader->getAttribLocation("in_Color");

    auto mapName = [&](int index) {
        shaderIds[index] = shader->getUniformLocation(uniformNames[index]);
    };

    mapName(ObjToWorld);
    mapName(WorldToView);
    mapName(Projection);
}

void destroyShaders(void)
{
    shader = nullptr;
}

void renderFunction(void)
{
    ++FrameCount;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    gu::drawBuffer(buffers);
}


void cleanup(void)
{
        destroyShaders();
        destroyVBO();
}

void appConfigure(GlApp::Configuration& config)
{
    config.window().name() = windowTitle;
}

void appInitialize(GlApp::AppState& state)
{
    createShaders();
    createTriangleVBO();
    initMatrices(shader.get());
    FrameCount = 0;
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
}

void appUpdate(GlApp::AppState& state)
{
    updateScene(state.elapsedTimeMs());
    renderFunction();
}

void appFinalize(GlApp::AppState& state)
{
    cleanup();
}

void appResize(GlApp::AppState& state,int width, int height){}

